contents
1. 제네릭이란?
- 제네릭(Generic) 은 자바에서 클래스, 메서드, 인터페이스의 타입을 '타입 파라미터'로 일반화하여, 다양한 참조 타입(객체)에 대해 재사용 가능한 코드를 작성할 수 있게 해주는 기능입니다.
- 주요 장점:
- 타입 안전성: 컴파일 시점에 타입 오류를 잡아 “ClassCastException”을 줄임.
- 코드 재사용성: 한 번 작성한 로직을 여러 타입에 활용.
- 캐스팅 불필요: 컬렉션 등에서 객체를 꺼낼 때 별도의 형변환 없이 타입 보장.
2. 기본 문법과 사용 예시
제네릭 클래스 예시
class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
System.out.println(stringBox.get()); // "Hello"
Box<Integer> intBox = new Box<>();
intBox.set(50);
System.out.println(intBox.get()); // 50
여기서 T는 타입 파라미터(placeholder)입니다. Box<String>이면 T는 String, Box<Integer>면 T는 Integer입니다.
3. 제네릭 메서드
public static <T> void printArray(T[] array) {
for (T item : array) System.out.println(item);
}
printArray(new String[]{"Alice", "Bob"});
printArray(new Integer[]{1, 2, 3});
메서드 선언 앞쪽에 <T> 타입 파라미터 명시, 다양한 타입의 배열에 활용 가능.
4. 제네릭 인터페이스
인터페이스도 제네릭 작성 가능:
public interface Pair<K, V> {
K getKey();
V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key; this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
K, V처럼 여러 타입 파라미터 동시 사용 가능.
5. 타입 제한(Bound): extends & super
제네릭 타입을 특정 상위클래스로 제한할 수 있습니다:
class NumberBox<T extends Number> { ... }
T는 반드시 Number의 하위클래스여야 함(Integer, Double 등).
와일드카드도 활용:
<?>— 모든 타입<? extends Type>— Type의 하위 타입<? super Type>— Type의 상위 타입(생산자/소비자 패턴)
6. 컬렉션과 제네릭
자바의 핵심 컬렉션(리스트, 맵 등)은 모두 제네릭으로 설계됨:
List<String> list = new ArrayList<>();
list.add("Apple");
String fruit = list.get(0); // 형변환 불필요
Map<String, List<String>> map = new HashMap<>();
원소 타입을 명확히 선언해 해당 타입만 추가 가능.
7. Raw Type(비제네릭 타입) vs 제네릭 타입
- Raw Type: 타입 파라미터 없이 선언한 제네릭 클래스(예:
List list = new ArrayList();) - Raw Type은 어떤 객체든 추가 가능해 비안전적, 런타임 오류 원인이 됨.
- 항상 타입 파라미터 명시(예:
List<String>,Box<Integer>)로 타입 안전성 확보.
8. 타입 추론 & 다이아몬드 연산자
자바 7부터, 인스턴스 생성 시 다이아몬드(<>) 연산자 사용 가능:
List<String> list = new ArrayList<>();
변수 선언에 따라 타입을 자동 추론.
9. 베스트 프랙티스
- 타입 파라미터는 대문자 한 글자(
T,E,K,V)로 작성. - 코드 재사용성을 위해 제네릭 메서드/클래스 활용 권장.
- Raw Type 지양.
- 타입 파라미터는 문서화 및 명확하게 작성.
- 유연성과 안전성을 위해 바운드 와일드카드 활용.
10. 실무 예시
- 컬렉션:
List<String>,Map<Integer, String>등 - Comparable 인터페이스:
public interface Comparable<T> { int compareTo(T o); } - 타입 안전 객체 저장용 커스텀 컨테이너
요약:
제네릭을 활용하면 타입에 대한 추상화와 유연성, 타입 안전성, 재사용성을 동시에 가질 수 있습니다. 컬렉션, 유틸리티 메서드, API 등 반복적 로직을 다양한 타입에 적용하면서도 안전하게 개발할 수 있습니다.
제네릭 메서드와 인터페이스에 대해 조금 더 알아보겠습니다.
1. 제네릭 메서드
- 제네릭 메서드란 메서드 선언부 앞에 타입 파라미터(
<T>등)를 명시하여, 클래스의 타입 파라미터와 무관하게 해당 메서드가 독립적인 타입들에 대해 동작할 수 있도록 만든 것입니다. - 문법상
<T>는 반환 타입 앞에 위치하며, 여러 타입 파라미터(T,K,V등)를 동시에 사용할 수도 있습니다.
기본 문법
public static <T> void myMethod(T param) { ... }
<T>를 반환 타입 앞에 선언하고, 파라미터나 결과로써 자유롭게 사용할 수 있습니다.
예시
public static <T> void printArray(T[] array) {
for (T item : array) {
System.out.println(item);
}
}
String[] names = {"Alice", "Bob"};
Integer[] nums = {1, 2, 3};
printArray(names); // Alice, Bob
printArray(nums); // 1, 2, 3
- 호출할 때 인자 타입으로부터 타입 추론이 자동으로 이루어지므로 명시적 타입 지정도 거의 필요 없습니다.
반환 타입이 제네릭인 예시
public static <T> T getFirst(T[] array) {
return array.length > 0 ? array : null;
}
- 파라미터 타입에 맞는 값을 반환할 수 있습니다.
바운드 사용 예시
public static <T extends Number> void showNum(T num) {
System.out.println(num.doubleValue());
}
T는 반드시Number의 하위 타입입니다(Integer 등).
다중 타입 파라미터 예시
public static <K,V> void printPair(K key, V value) {
System.out.println(key + " : " + value);
}
- 여러 타입 파라미터를 동시에 사용할 수 있습니다.
2. 제네릭 인터페이스
- 제네릭 인터페이스란 타입 파라미터를 사용해서 구현 클래스 또는 인스턴스에서 실제 타입을 지정하도록 만드는 구조입니다. 컬렉션 프레임워크(List, Map 등)에서 자주 활용됩니다.
기본 문법
public interface Container<T> {
void set(T value);
T get();
}
예시
public interface Pair<K, V> {
K getKey();
V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private final K key;
private final V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
Pair<String, Integer> pair = new OrderedPair<>("나이", 30);
System.out.println(pair.getKey()); // 나이
System.out.println(pair.getValue()); // 30
- 실제 구현 시 구체 타입을 지정하면 타입 안전한 코드가 됩니다.
바운드 타입 인터페이스 예시
public interface NumberContainer<T extends Number> {
void setNumber(T num);
T getNumber();
}
- 반드시 Number의 하위 타입만 지정 가능함.
3. 설계/활용 포인트
- 타입 추론: 제네릭 메서드를 호출할 때 자바가 타입을 거의 자동 추론해줍니다.
- 다중 파라미터: 복수 타입 입력/결과가 필요한 API에 적합.
- 컬렉션/비교 메소드 등: 핵심 API와 유틸리티를 위해 필수적.
요약
제네릭 메서드는 반환 타입 앞에 <T>를 명시하여 다양한 타입에 안전하게 대응하며, 제네릭 인터페이스는 구현 시 타입 명시를 통해 재사용과 안정성을 높입니다. 컬렉션 및 비교, 유틸 API 등 자바에서 필수적인 설계 패턴입니다.
Java의 제네릭 용법에서 경계(Boundaries, extends & super) 에 대해 조금 더 알아보겠습니다.
1. 상한 경계(Upper Bound): extends
T extends Type를 선언하면 T는 Type 또는 그 하위 클래스여야만 합니다.- 상한 경계를 사용하면, 특정 클래스(혹은 인터페이스)의 하위 타입만 인자로 받을 수 있도록 제한해 코드의 타입 안정성을 높일 수 있습니다.
기본 문법
class Box<T extends Number> { ... }
- T는 Number 또는 그 하위 타입만 가능 (예: Integer, Double, Float 등).
예시
public class Bound<T extends A> {
private T objRef;
public Bound(T obj) { this.objRef = obj; }
public void doRunTest() { objRef.displayClass(); }
}
class A { public void displayClass() { System.out.println("A"); } }
class B extends A { public void displayClass() { System.out.println("B"); } }
class C extends A { public void displayClass() { System.out.println("C"); } }
public class BoundedClass {
public static void main(String[] args) {
Bound<C> bec = new Bound<>(new C()); // OK
bec.doRunTest(); // "C"
Bound<B> beb = new Bound<>(new B()); // OK
beb.doRunTest(); // "B"
Bound<A> bea = new Bound<>(new A()); // OK
bea.doRunTest(); // "A"
// Bound<String> bes = new Bound<>(new String()); // 컴파일 오류 (A를 상속하지 않음)
}
}
A 혹은 그 하위 클래스만 Bound<?>에 쓸 수 있고, 다른 타입은 컴파일 오류.
인터페이스/다중 경계
<T extends SuperClass & Interface1 & Interface2>
- 반드시 클래스가 첫번째, 뒤는 인터페이스만 올 수 있습니다.
2. 하한 경계(Lower Bound): super
? super Type는 Type 혹은 그 상위 클래스만 인자로 받을 수 있게 제한합니다.- 주로 생산자-소비자 패턴에서 소비자(쓰기) 메서드에 활용합니다.
기본 문법
List<? super Integer> list;
- 반드시 Integer 혹은 그 상위 타입(Number, Object 등)만 담을 수 있습니다.
예시
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 5; i++)
list.add(i);
}
public static void main(String[] args) {
List<Number> numberList = new ArrayList<>();
addNumbers(numberList); // 가능
System.out.println(numberList); // [1, 2, 3, 4, 5]
// List<Double> doubleList = new ArrayList<>();
// addNumbers(doubleList); // 컴파일 오류
}
List<? super Integer>에는 Integer, Number, Object만 넣을 수 있습니다.- Double과 같이 Integer와 직접 상속 관계가 없는 타입은 불가.
3. 다중 경계(Multiple Bounds)
<T extends ClassName & Interface1 & Interface2>
- 한 번에 클래스는 하나만, 인터페이스는 여러 개 지정 가능.
4. 와일드카드와 경계의 쓰임
| 문법 | 의미 | 활용 |
|---|---|---|
<T extends Number> |
T는 Number 또는 하위클래스만 | 타입 안정성 보장, 읽기 작업 |
<? extends Number> |
Number 또는 하위타입 만 가능 | 읽기 작업 증가, 불변에 적합 |
<? super Integer> |
Integer 혹은 그 상위타입만 가능 | 쓰기 작업 증가, 소비자 패턴 |
- 상한 경계는 읽기 전용(데이터를 꺼낼 때), 하한 경계는 쓰기 전용(데이터를 넣을 때)으로 많이 활용합니다.
결론
상한 경계(extends)는 지정된 타입의 하위 타입만 허용하고, 하한 경계(super)는 지정된 타입의 상위 타입만 허용하여 타입 안정성과 유연성을 동시에 제공합니다. 다중 경계는 여러 인터페이스/한 클래스까지 묶어 더욱 강력한 제네릭 제어가 가능합니다.
references